{-# LANGUAGE MagicHash, CPP, MultiParamTypeClasses, OverloadedStrings,
             TypeSynonymInstances , FlexibleInstances,
             GeneralizedNewtypeDeriving, BangPatterns, TypeOperators,
             KindSignatures, DefaultSignatures, FlexibleInstances,
             TypeSynonymInstances, FlexibleContexts, ScopedTypeVariables,
             TupleSections #-}
-- | Handling of Javascript-native binary blobs.
--
-- Generics borrowed from the binary package by Lennart Kolmodin (released under BSD3)
module Haste.Binary
  ( -- * High level binary API
    MonadConc, Binary (..), Blob, BlobData, encode, decode, decodeBlob
  , module Haste.Binary.Put
  , module Haste.Binary.Get
  , Word8, Word16, Word32, Word64
  , Int8, Int16, Int32, Int64

    -- * Working with raw blobs
  , Ix, ArrView
  , getBlobData, getBlobText', getBlobText
  , blobSize, blobDataSize, toByteString, fromByteString, toBlob, strToBlob
  , toUArray, fromUArray
  ) where
import Data.Array.Unboxed
import Data.Bits
import Data.Char
import Data.Int
import Data.Word
import GHC.Fingerprint.Type
import GHC.Generics
import qualified Haste.JSString as J (length)
import Haste.Prim
import Haste.Concurrent
import Haste.Prim.Foreign hiding (get)
import Haste.Binary.Types
import Haste.Binary.Put
import Haste.Binary.Get
#ifndef __HASTE__
import qualified Data.ByteString.Lazy.Char8 as BS (unpack)
#endif

-- | Retrieve the raw data from a blob.
getBlobData  :: MonadConc m => Blob -> m BlobData
-- | Interpret a blob as UTF-8 text, as a JSString.
getBlobText' :: MonadConc m => Blob -> m JSString

#ifdef __HASTE__
getBlobData b = liftCIO $ do
    res <- newEmptyMVar
    liftIO $ convertBlob b (mkBlobData res (blobSize b))
    takeMVar res
  where
    mkBlobData res len x = concurrent $ do
      putMVar res (BlobData 0 len x)

    convertBlob :: Blob -> (JSAny -> IO ()) -> IO ()
    convertBlob = ffi "(function(b,cb){var r=new FileReader();r.onload=function(){cb(new DataView(r.result));};r.readAsArrayBuffer(b);})"

getBlobText' b = liftCIO $ do
    res <- newEmptyMVar
    liftIO $ convertBlob b (concurrent . putMVar res)
    takeMVar res
  where
    convertBlob :: Blob -> (JSString -> IO ()) -> IO ()
    convertBlob = ffi "(function(b,cb){var r=new FileReader();r.onload=function(){cb(r.result);};r.readAsText(b);})"
#else
getBlobData (Blob b) = return (BlobData b)
getBlobText' (Blob b) = return . toJSStr $ BS.unpack b
#endif


-- | Interpret a blob as UTF-8 text.
getBlobText :: MonadConc m => Blob -> m String
getBlobText b = getBlobText' b >>= return . fromJSStr

-- | Somewhat efficient serialization/deserialization to/from binary Blobs.
--   The layout of the binaries produced/read by get/put and encode/decode may
--   change between versions. If you need a stable binary format, you should
--   make your own using the primitives in Haste.Binary.Get/Put.
class Binary a where
  get :: Get a
  put :: a -> Put

  default put :: (Generic a, GBinary (Rep a)) => a -> Put
  put = gput . from

  default get :: (Generic a, GBinary (Rep a)) => Get a
  get = to `fmap` gget

-- | Generic version
class GBinary f where
    gput :: f t -> Put
    gget :: Get (f t)

instance Binary Bool where
  put True = putWord8 1
  put _    = putWord8 0
  get = do
    n <- getWord8
    case n of
      0 -> pure False
      1 -> pure True
      _ -> fail $ "Not a valid Bool: " ++ show n

instance Binary Word8 where
  put = putWord8
  get = getWord8

instance Binary Word16 where
  put = putWord16le
  get = getWord16le

instance Binary Word32 where
  put = putWord32le
  get = getWord32le

instance Binary Word64 where
  get = do
    lo <- get :: Get Word32
    hi <- get :: Get Word32
    return $ fromIntegral lo .|. shiftL (fromIntegral hi) 32
  put x = do
    put (fromIntegral x :: Word32)
    put (fromIntegral (shiftR x 32) :: Word32)

instance Binary Int8 where
  put = putInt8
  get = getInt8

instance Binary Int16 where
  put = putInt16le
  get = getInt16le

instance Binary Int32 where
  put = putInt32le
  get = getInt32le

instance Binary Int64 where
  get = do
    lo <- get :: Get Int32
    hi <- get :: Get Int32
    return $ fromIntegral lo .|. shiftL (fromIntegral hi) 32
  put x = do
    put (fromIntegral x :: Int32)
    put (fromIntegral (shiftR x 32) :: Int32)

instance Binary Int where
  put = putInt32le . fromIntegral
  get = fromIntegral <$> getInt32le

instance Binary Float where
  put = putFloat32le
  get = getFloat32le

instance Binary Double where
  put = putFloat64le
  get = getFloat64le

instance Binary Fingerprint where
  get = Fingerprint <$> get <*> get
  put (Fingerprint a b) = put a >> put b

instance (Binary a, Binary b) => Binary (a, b) where
  put (a, b) = put a >> put b
  get = (,) <$> get <*> get
instance (Binary a, Binary b, Binary c) => Binary (a, b, c) where
  put (a, b, c) = put a >> put b >> put c
  get = (,,) <$> get <*> get <*> get
instance (Binary a, Binary b, Binary c, Binary d) => Binary (a, b, c, d) where
  put (a, b, c, d) = put a >> put b >> put c >> put d
  get = (,,,) <$> get <*> get <*> get <*> get
instance (Binary a, Binary b, Binary c, Binary d, Binary e)
         => Binary (a, b, c, d, e) where
  put (a, b, c, d, e) = put a >> put b >> put c >> put d >> put e
  get = (,,,,) <$> get <*> get <*> get <*> get <*> get
instance (Binary a, Binary b, Binary c, Binary d, Binary e, Binary f)
         => Binary (a, b, c, d, e, f) where
  put (a, b, c, d, e, f) = put a >> put b >> put c >> put d >> put e >> put f
  get = (,,,,,) <$> get <*> get <*> get <*> get <*> get <*> get
instance (Binary a, Binary b, Binary c, Binary d, Binary e, Binary f, Binary g)
         => Binary (a, b, c, d, e, f, g) where
  put (a, b, c, d, e, f, g) = put a>>put b>>put c>>put d>>put e>>put f>>put g
  get = (,,,,,,) <$> get <*> get <*> get <*> get <*> get <*> get <*> get

instance Binary a => Binary (Maybe a) where
  put (Just x) = putWord8 1 >> put x
  put _        = putWord8 0
  get = do
    tag <- getWord8
    case tag of
      0 -> return Nothing
      1 -> Just <$> get
      _ -> fail "Wrong constructor tag when reading Maybe value!"

instance (Binary a, Binary b) => Binary (Either a b) where
  put (Left x)  = putWord8 0 >> put x
  put (Right x) = putWord8 1 >> put x
  get = do
    tag <- getWord8
    case tag of
      0 -> Left <$> get
      1 -> Right <$> get
      _ -> fail "Wrong constructor tag when reading Either value!"

instance Binary () where
  put _ = return ()
  get = return ()

instance Binary a => Binary [a] where
  put xs = do
    putWord32le (fromIntegral $ length xs)
    mapM_ put xs
  get = getWord32le >>= getMany

instance Binary JSString where
  {-# NOINLINE put #-}
  put s = do
    putWord32le $ fromIntegral $ J.length s
    putJSString s
  {-# NOINLINE get #-}
  get = get >>= getJSString
    
instance Binary Blob where
  {-# NOINLINE put #-}
  put b = do
    put (blobSize b)
    putBlob b
  {-# NOINLINE get #-}
  get = do
    sz <- get
    bd <- getBytes sz
    return $ toBlob bd

instance Binary Char where
  put = put . ord
  get = get >>= \x ->
    case chr x of
      !x' -> return x'

-- Borrowed from the @binary@ package.
instance (Binary i, Ix i, Binary e, IArray UArray e) => Binary (UArray i e) where
  put a = do
    put (bounds a)
    put (rangeSize $ bounds a)
    mapM_ put (elems a)
  get = do
    bs <- get
    n  <- get
    xs <- getMany n
    return (listArray bs xs)

-- | 'getMany n' get 'n' elements in order, without blowing the stack.
{-# INLINE getMany #-}
getMany :: Binary a => Word32 -> Get [a]
getMany n = go [] n
 where
    go xs 0 = return $! reverse xs
    go xs i = do x <- get
                 -- we must seq x to avoid stack overflows due to laziness in
                 -- (>>=)
                 x `seq` go (x:xs) (i-1)

-- | Encode any serializable data into a 'Blob'.
encode :: Binary a => a -> Blob
encode x = runPut (put x)

-- | Decode any deserializable data from a 'BlobData'.
decode :: Binary a => BlobData -> Either JSString a
decode = runGet get

-- | Decode a 'Blob' into some deserializable value, inconveniently locked up
--   inside the 'CIO' monad (or any other concurrent monad) due to the somewhat
--   special way JavaScript uses to deal with binary data.
decodeBlob :: (MonadConc m, Binary a) => Blob -> m (Either JSString a)
decodeBlob b = getBlobData b >>= return . decode

-- Type without constructors
instance GBinary V1 where
    gput _ = return ()
    gget   = return undefined

-- Constructor without arguments
instance GBinary U1 where
    gput U1 = return ()
    gget    = return U1

-- Product: constructor with parameters
instance (GBinary a, GBinary b) => GBinary (a :*: b) where
    gput (x :*: y) = gput x >> gput y
    gget = (:*:) <$> gget <*> gget

-- Metadata (constructor name, etc)
instance GBinary a => GBinary (M1 i c a) where
    gput = gput . unM1
    gget = M1 <$> gget

-- Constants, additional parameters, and rank-1 recursion
instance Binary a => GBinary (K1 i a) where
    gput = put . unK1
    gget = K1 <$> get

-- Borrowed from the cereal package.

-- The following GBinary instance for sums has support for serializing
-- types with up to 2^64-1 constructors. It will use the minimal
-- number of bytes needed to encode the constructor. For example when
-- a type has 2^8 constructors or less it will use a single byte to
-- encode the constructor. If it has 2^16 constructors or less it will
-- use two bytes, and so on till 2^64-1.
--
-- NB: changed to 2^32-1 constructors

#define GUARD(WORD) (size - 1) <= fromIntegral (maxBound :: WORD)
#define PUTSUM(WORD) GUARD(WORD) = putSum (0 :: WORD) (fromIntegral size)
#define GETSUM(WORD) GUARD(WORD) = (get :: Get WORD) >>= checkGetSum (fromIntegral size)

instance ( GSum     a, GSum     b
         , GBinary a, GBinary b
         , SumSize    a, SumSize    b) => GBinary (a :+: b) where
    gput | PUTSUM(Word8) | PUTSUM(Word16) | PUTSUM(Word32) --  | PUTSUM(Word64)
         | otherwise = sizeError "encode" size
      where
        size = unTagged (sumSize :: Tagged (a :+: b) Word32)
    {-# INLINE gput #-}

    gget | GETSUM(Word8) | GETSUM(Word16) | GETSUM(Word32) --  | GETSUM(Word64)
         | otherwise = sizeError "decode" size
      where
        size = unTagged (sumSize :: Tagged (a :+: b) Word32)
    {-# INLINE gget #-}

sizeError :: Show size => String -> size -> error
sizeError s size =
    error $ "Can't " ++ s ++ " a type with " ++ show size ++ " constructors"

------------------------------------------------------------------------

checkGetSum :: (Ord word, Num word, Bits word, GSum f)
            => word -> word -> Get (f a)
checkGetSum size code | code < size = getSum code size
                      | otherwise   = fail "Unknown encoding for constructor"
{-# INLINE checkGetSum #-}

class GSum f where
    getSum :: (Ord word, Num word, Bits word) => word -> word -> Get (f a)
    putSum :: (Num w, Bits w, Binary w) => w -> w -> f a -> Put

instance (GSum a, GSum b, GBinary a, GBinary b) => GSum (a :+: b) where
    getSum !code !size | code < sizeL = L1 <$> getSum code           sizeL
                       | otherwise    = R1 <$> getSum (code - sizeL) sizeR
        where
          sizeL = size `shiftR` 1
          sizeR = size - sizeL
    {-# INLINE getSum #-}

    putSum !code !size s = case s of
                             L1 x -> putSum code           sizeL x
                             R1 x -> putSum (code + sizeL) sizeR x
        where
          sizeL = size `shiftR` 1
          sizeR = size - sizeL
    {-# INLINE putSum #-}

instance GBinary a => GSum (C1 c a) where
    getSum _ _ = gget
    {-# INLINE getSum #-}

    putSum !code _ x = put code *> gput x
    {-# INLINE putSum #-}

------------------------------------------------------------------------

class SumSize f where
    sumSize :: Tagged f Word32

newtype Tagged (s :: * -> *) b = Tagged {unTagged :: b}

instance (SumSize a, SumSize b) => SumSize (a :+: b) where
    sumSize = Tagged $ unTagged (sumSize :: Tagged a Word32) +
                       unTagged (sumSize :: Tagged b Word32)

instance SumSize (C1 c a) where
    sumSize = Tagged 1
